# ProjectionMappingFinder.java

    package org.codefilarete.stalactite.spring.repository.query.projection;

    import java.util.IdentityHashMap;

    import org.codefilarete.reflection.AccessorChain;
    import org.codefilarete.reflection.Accessors;
    import org.codefilarete.stalactite.engine.runtime.AdvancedEntityPersister;
    import org.codefilarete.stalactite.engine.runtime.query.AggregateAccessPointToColumnMapping;
    import org.codefilarete.stalactite.query.api.JoinLink;
    import org.codefilarete.stalactite.query.api.Selectable;
    import org.springframework.data.mapping.PropertyPath;
    import org.springframework.data.mapping.context.MappingContext;
    import org.springframework.data.projection.EntityProjection;
    import org.springframework.data.projection.EntityProjectionIntrospector;
    import org.springframework.data.projection.ProjectionFactory;
    import org.springframework.data.relational.core.mapping.RelationalMappingContext;

    /**
     * Helper class to provide the {@link Selectable}s necessary to build some projection instance from a database query (native or not).
     * The columns are taken from the aggregate type.
     *
     * @param <C> the aggregate (root entity) type
     * @author Guillaume Mary
     */
    public class ProjectionMappingFinder<C> {

    	private final AggregateAccessPointToColumnMapping<C> aggregateColumnMapping;
    	private final EntityProjectionIntrospector entityProjectionIntrospector;
    	private final Class<C> aggregateType;

    	/**
    	 * Initiates an instance capable of collecting aliases and their matching property of the aggregate of the given entityPersister
    	 *
    	 * @param factory
    	 * @param entityPersister
    	 */
    	public ProjectionMappingFinder(ProjectionFactory factory, AdvancedEntityPersister<C, ?> entityPersister) {
    		// The projection is closed: it means there's not @Value on the interface, so we can use Spring property introspector to look up for
    		// properties to select in the query
    		// If the projection is open (any method as a @Value on it), then, because Spring can't know in advance which field will be required to
    		// evaluate the @Value expression, we must retrieve the whole aggregate as entities.
    		// se https://docs.spring.io/spring-data/jpa/reference/repositories/projections.html
    		EntityProjectionIntrospector.ProjectionPredicate isProjectionTest = (returnType, domainType)
    				-> !returnType.isAssignableFrom(domainType);

    		// We create a dumb MappingContext (un-typed) to "just make it work". I'm unsure about the way it works, but implementing it is a bit
    		// complex for Stalactite due to the need to have PersistentEntity which requires also some concepts like PreferredConstructor or InstanceCreatorMetadata
    		// => to be implemented if this dumb implementation is unsufficient
    		MappingContext mappingContext = new RelationalMappingContext();
    		this.entityProjectionIntrospector = EntityProjectionIntrospector.create(factory, isProjectionTest, mappingContext);
    		this.aggregateColumnMapping = entityPersister.getEntityFinder().newCriteriaSupport().getEntityCriteriaSupport().getAggregateColumnMapping();
    		this.aggregateType = entityPersister.getClassToPersist();
    	}

    	/**
    	 * Extracts the {@link JoinLink} and {@link PropertyPath} from the {@link ProjectionFactory} and {@link AdvancedEntityPersister} of construction time.
    	 * The algorithm is based on Spring property introspection to make us match the way it detects the properties of a projection. Thus, we are much more
    	 * compatible with Spring Data than if we re-invent the wheel. Meanwhile, the will to re-invent it is very tempting because the algorithm is unclear,
    	 * not well-documented, with a lot of closed / private classes.
    	 *
    	 * @param projectionTypeToIntrospect the projection type to introspect
    	 * @return a map of {@link JoinLink} to {@link PropertyPath}
    	 */
    	public IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> lookup(Class<?> projectionTypeToIntrospect) {
    		IdentityHashMap<JoinLink<?, ?>, AccessorChain<C, ?>> result = new IdentityHashMap<>();

    		EntityProjection<?, C> projectionTypeIntrospection = entityProjectionIntrospector.introspect(projectionTypeToIntrospect, aggregateType);
    		projectionTypeIntrospection.forEachRecursive(projectionProperty -> {
    			AccessorChain accessorChain = convertToAccessorChain(projectionProperty.getPropertyPath());
    			try {
    				JoinLink<?, ?> selectable = aggregateColumnMapping.giveColumn(accessorChain.getAccessors());
    				result.put(selectable, accessorChain);
    			} catch (RuntimeException e) {
    				// MADE TO AVOID Error while looking for column of o.c.s.e.m.Republic.getPrimeMinister() : it is not declared in mapping of o.c.s.e.m.Republic
    			}
    		});
    		return result;
    	}

    	private AccessorChain<?, ?> convertToAccessorChain(PropertyPath propertyPath2) {
    		AccessorChain<?, ?> result = new AccessorChain<>();
    		propertyPath2.forEach(propertyPath -> {
    			result.add(Accessors.accessor(propertyPath.getOwningType().getType(), propertyPath.getSegment(), propertyPath.getType()));
    		});
    		return result;
    	}
    }